--- title: "05-JWT + Redis 双重会话管理 学习笔记" created: 2025-12-12 aliases: - JWT + Redis 双重会话管理 学习笔记 tags: - 项目 --- # JWT + Redis 双重会话管理 学习笔记 ## **前言** 本笔记基于对微服务架构中会话管理方案的学习与思考,重点探讨了 **JWT + Redis 双重验证机制** 的设计原理、架构权衡以及鉴权扩展方案。 ## **一、核心设计:JWT + Redis 双重会话管理** ### **1.1 设计目标** 这个设计旨在解决 **纯 JWT** 和 **纯 Session** 各自的痛点: | **方案** | **优点** | **缺点** | | --- | --- | --- | | 纯 JWT | 无状态、易于横向扩展 | 无法主动注销、Token 较长 | | 纯 Session | 可随时注销、可存储丰富信息 | 需要维护服务端状态、分布式困难 | | **JWT + Redis** | 兼顾两者优点 | 复杂度稍高 | ### **1.2 代码实现** ```text // ==================== 登录时:同时生成 JWT 和存储 Redis ==================== // 1. 存入 Redis(存储用户完整信息) redisCache.set( RedisKeyBuild.createRedisKey(RedisKeyManage.USER_LOGIN, code, user.getId()), user, tokenExpireTime, TimeUnit.MINUTES ); // 2. 生成 JWT(只包含 userId 等基本信息) userLoginVo.setToken(createToken(user.getId(), getChannelDataByCode(code).getTokenSecret())); ``` ```java // ==================== 网关验证时:两者都要检查 ==================== public UserVo getUser(String token, String code, String tokenSecret) { // 第一重:解析 JWT 获取 userId(验证签名完整性) String userId = parseToken(token, tokenSecret); // 第二重:检查 Redis 是否存在登录态 if (StringUtil.isNotEmpty(userId)) { userVo = redisCache.get( RedisKeyBuild.createRedisKey(RedisKeyManage.USER_LOGIN, code, userId), UserVo.class ); } // 两重验证都通过才放行 return Optional.ofNullable(userVo) .orElseThrow(() -> new DaMaiFrameException(BaseCode.LOGIN_USER_NOT_EXIST)); } ``` ## **二、核心问题探讨** ### **2.1 我的疑问** > ***我的理解是**:用 Token 而不是 Session 其实就是为了**无状态**,无需在服务端维护连接信息,节省空间。但缺点是只能解析出 id,一些信息得通过 id 查表才能拿到,这些可能会损耗一定资源。而 Session 可以存储一些信息在登录态里面,登录成功后无需查数据库。* > > *但是这里把用户信息存在 Redis 里,岂不是**又花了维护连接的空间,还花了查询的资源和时间**,仅仅只是为了能踢人?那为什么不直接用 **Session + Redis** 呢?* ### **2.2 架构权衡分析** 这个质疑确实直击架构本质。在**微服务高并发**的架构背景下,选择 JWT + Redis 而不是 Session + Redis,基于以下 **4 个核心维度**的考量: #### **维度一:微服务架构下的"透传"优势(核心差异)** **Session + Redis 方案的问题:** ```text 用户请求 → 网关(查Redis) → 订单服务(再查Redis) → 票务服务(再查Redis) ↓ ↓ ↓ Redis压力 ×1 Redis压力 ×2 Redis压力 ×3 ``` **JWT + Redis 方案(本方案):** ```text 用户请求 → 网关(查Redis验证,解析JWT) → 订单服务(直接用JWT信息) → 票务服务(直接用JWT信息) ↓ ↓ ↓ Redis压力 ×1 无需查Redis 无需查Redis ``` > 💡 核心收益:网关负责"脏活累活",下游服务通过 CPU 计算验证 JWT 签名即可,极大减少了 Redis 的 QPS 压力,实现了"一次认证,处处可用"。 #### **维度二:多端兼容性** | **方案** | **Web 浏览器** | **iOS/Android App** | **小程序** | **第三方接口** | | --- | --- | --- | --- | --- | | Session/Cookie | ✅ 原生支持 | ⚠️ 需要 CookieJar | ⚠️ 跨域问题 | ❌ 不友好 | | JWT (Header) | ✅ 支持 | ✅ 标准 HTTP | ✅ 标准 HTTP | ✅ 标准 HTTP | > *💡 如果项目业务 App 端流量很大,**Token 形式远优于 Cookie/Session 形式**。* #### **维度三:"柔性鉴权"与降级能力** 代码中有 `skipCheckToken` 和 `checkNeedUserId` 逻辑: ```java boolean skipCheckTokenResult = skipCheckToken(url); // ... if (!skipCheckTokenResult) { // 强制查 Redis UserVo userVo = tokenService.getUser(...); } ``` **高并发降级场景(如 Redis 响应变慢):** | **方案** | **降级能力** | | --- | --- | | Session 方案 | ❌ 系统直接不可用 | | JWT + Redis | ✅ 非核心业务可只校验 JWT 签名,跳过 Redis 检查 | > *💡 虽然降级时无法"踢人",但至少保证了**业务不崩,用户能看能刷**。* #### **维度四:解决纯 JWT 的"无法撤销"问题** 对于涉及资金交易(买票)的系统,这是安全刚需: | **场景** | **纯 JWT** | **JWT + Redis** | | --- | --- | --- | | 用户手机丢失 | ❌ 无法强制下线 | ✅ 删除 Redis Key 即可 | | 封禁黄牛账号 | ❌ Token 未过期仍有效 | ✅ 删除 Redis Key 即可 | | 用户主动退出 | ❌ Token 仍然有效 | ✅ 删除 Redis Key 即可 | > *💡 Redis 的存储成本,是为"安全可控"支付的保险费。* ### **2.3 方案对比总结** | **维度** | **纯 Session + Redis** | **纯 JWT** | **JWT + Redis (本方案)** | | --- | --- | --- | --- | | 存储压力 | 高 (Redis) | 无 | 中/高 (Redis) | | 带宽压力 | 低 (SessionID 短) | 高 (Token 长) | 高 (Token 长) | | 多端兼容 | 差 (依赖 Cookie) | 好 (Header) | 好 (Header) | | 微服务传递 | 难 (需共享 Redis) | 易 (自带信息) | 易 (网关查完后下游自带信息) | | 踢人/注销 | ✅ 支持 | ❌ 不支持 | ✅ 支持 | | 降级能力 | ❌ 无 (必须查库) | ✅ 强 (只验签) | ✅ 强 (可灵活配置) | ### **2.4 结论** > *📌 如果是**单体 Web 应用**,Session + Redis 确实是更简单的选择。* > > *📌 但因为这是**微服务 + 多端 App + 高并发抢票**场景,"微服务间的信息透传" 和 "多端兼容" 的权重压倒了 "节省 Redis 空间和 Token 流量" 的成本。* > > *📌 这是一种用 **"Redis 空间"和"Token 流量"** 换取 **"架构灵活性"和"服务解耦"** 的权衡设计。* ## **三、鉴权逻辑分析** ### **3.1 我的疑问** > *这套系统里是不是没有严格的鉴权逻辑?之前的单体项目是使用 **RBAC 模型**,借助 Redis 存放 Session 和 LoginVo(里面包含了用户的身份集合和权限集合),每次请求打到就 AOP 拦截先校验,通过注解的形式打在某个接口上实现接口级别的精细鉴权。* > > *这里 Redis 里也存了 UserVo,貌似可以扩展到那种形式?* ### **3.2 To C 与 To B 的鉴权差异** #### **To B(管理后台)—— 功能权限** ```text 场景:内部系统、ERP、CMS 特点:用户分三六九等(超级管理员、财务、运营、普通员工) 核心:功能权限(Functional Authority) 实现:@PreAuthorize("hasRole('ADMIN')"),基于 RBAC 模型 ``` #### **To C(消费者端)—— 数据权限** ```text 场景:数百万普通用户来抢票 特点:99.9% 的用户角色都是"普通会员" 核心:数据权限(Data Authority) 实现:代码里的逻辑校验(currentUserId == order.userId) ``` > 💡 这套系统目前展示的是 C 端逻辑,所以没有看到复杂的 RBAC。但在后台管理模块中,RBAC 方案是标准答案。 ### **3.3 如何扩展 RBAC 鉴权** #### **Step 1:扩充 UserVo** ```java public class UserVo { private Long id; private String username; // 扩展:角色集合 private Set roles; // 扩展:权限标识集合 (e.g., "program:add", "order:export") private Set permissions; } ``` #### **Step 2:登录时注入权限** ```java // 管理员登录成功后 UserVo userVo = new UserVo(); userVo.setId(user.getId()); userVo.setUsername(user.getUsername()); // 查询角色和权限 userVo.setRoles(roleService.getRolesByUserId(user.getId())); userVo.setPermissions(permissionService.getPermissionsByUserId(user.getId())); // 存入 Redis redisCache.set(key, userVo, tokenExpireTime, TimeUnit.MINUTES); ``` #### **Step 3:鉴权拦截位置选择** | **位置** | **适用场景** | **优点** | **缺点** | | --- | --- | --- | --- | | **网关层鉴权** | 粗粒度拦截 | 无权限请求进不去微服务,节省资源 | 需维护 URL-权限映射,配置繁琐 | | **服务内 AOP** | 细粒度拦截 | 开发体验好,权限跟着接口走 | 请求已进入服务内部 | **推荐组合**: ```text 网关:全局黑白名单 + 登录态检查 服务内 AOP:业务强相关的细粒度拦截(如 @RequiresPermissions("user:delete")) ``` ### **3.4 微服务下的特殊挑战** #### **挑战一:内存膨胀问题** | **场景** | **用户量** | **Redis 存储策略** | | --- | --- | --- | | 单体 To B | 几千内部员工 | 完整权限列表,无压力 | | 微服务 To C | 1000万活跃用户 | ⚠️ 每人存权限列表会爆内存 | **优化方案**: - C 端用户:Redis 只存基本信息,不存权限 - B 端/VIP 用户:才存储权限列表 - 使用 **BitMap** 或 **角色 ID** 代替具体的权限字符串列表 #### **挑战二:权限变更的实时性** **问题场景:** ```text 1. 在数据库里删除了管理员的权限 2. 但他的 UserVo 还在 Redis 里(TTL 30分钟) 3. 结果:他依然拥有权限,直到 Redis 过期 ``` 解决方案: ```java // 修改权限时,主动删除/更新该用户在 Redis 中的 UserVo 缓存 public void updateUserPermissions(Long userId, Set newPermissions) { // 1. 更新数据库 permissionMapper.updateByUserId(userId, newPermissions); // 2. 删除 Redis 缓存(强制下次请求重新加载) redisCache.delete(RedisKeyBuild.createRedisKey(RedisKeyManage.USER_LOGIN, code, userId)); } ``` ## **四、核心理解总结** ### **4.1 会话管理的本质** ```text ┌─────────────────────────────────────────────────────────────┐ │ │ │ Token(JWT)= 钥匙(Key) │ │ - 只是一个凭证,用于证明"我是谁" │ │ - 存储在客户端 │ │ │ │ Redis = 锁芯(Session Store) │ │ - 真正的状态存储,包含用户详细信息 │ │ - 存储在服务端,可随时控制(踢人、封禁) │ │ │ └─────────────────────────────────────────────────────────────┘ ``` ### **4.2 双重验证流程** ```text 客户端请求 │ ▼ ┌───────────────┐ │ 携带 JWT Token │ └───────┬───────┘ │ ▼ ┌───────────────────────┐ │ 第一重:验证 JWT 签名 │ ← CPU 计算,验证 Token 未被篡改 │ (解析出 userId) │ └───────────┬───────────┘ │ ▼ ┌───────────────────────┐ │ 第二重:查询 Redis │ ← IO 操作,验证登录态是否存在 │ (检查是否被踢/过期) │ └───────────┬───────────┘ │ ┌───────┴───────┐ │ │ ▼ ▼ ✅ 放行 ❌ 拒绝 ``` ### **4.3 鉴权层次** | **层次** | **关注点** | **实现方式** | | --- | --- | --- | | **认证(Authentication)** | 你是谁? | JWT 解析 + Redis 状态检查 | | **授权(Authorization)** | 你能做什么? | RBAC + AOP 注解 | | **数据权限(Data Scope)** | 你能看哪些数据? | 业务代码逻辑校验 | ## **五、设计启示** ### **5.1 架构选型不是非黑即白** > *没有"最好"的方案,只有"最适合"的方案。JWT + Redis 看似"两者缺点都占了",但在特定场景下却是最优解。* ### **5.2 理解场景是关键** ```text 单体应用 + Web 端 → Session + Redis 就够了 微服务 + 多端 + 高并发 → JWT + Redis 更合适 ``` ### **5.3 安全与性能的权衡** > *Redis 存储成本 = 安全可控的保险费* 在涉及资金交易的系统中,"能踢人"不是可选项,而是刚需。 --- **企业级项目导航**:⬅️ [[04-(缓存预热)购票人业务架构分析|04-(缓存预热)购票人业务架构分析]] | 05-JWT + Redis 双重会话管理 学习笔记 | ➡️ [[06-Lua 深度学习笔记|06-Lua 深度学习笔记]]